﻿using System.Collections;

namespace TheSalLab.MauiInfiniteScrolling;

public class MauiInfiniteScrollBehavior : Behavior<ListView> {
    public static readonly BindableProperty IsLoadingMoreProperty =
        BindableProperty.Create(nameof(IsLoadingMore), typeof(bool),
            typeof(MauiInfiniteScrollBehavior), default(bool),
            BindingMode.OneWayToSource);

    public static readonly BindableProperty ItemsSourceProperty =
        BindableProperty.Create(nameof(ItemsSource), typeof(IEnumerable),
            typeof(MauiInfiniteScrollBehavior), default(IEnumerable),
            BindingMode.OneWay, propertyChanged: OnItemsSourceChanged);

    private bool _isLoadingMoreFromScroll;

    private bool _isLoadingMoreFromLoader;

    private ListView _associatedListView;

    public bool IsLoadingMore {
        get => (bool)GetValue(IsLoadingMoreProperty);
        private set => SetValue(IsLoadingMoreProperty, value);
    }

    private IEnumerable ItemsSource =>
        (IEnumerable)GetValue(ItemsSourceProperty);

    protected override void OnAttachedTo(ListView bindable) {
        base.OnAttachedTo(bindable);

        _associatedListView = bindable;

        SetBinding(ItemsSourceProperty,
            new Binding(ListView.ItemsSourceProperty.PropertyName,
                source: _associatedListView));

        bindable.BindingContextChanged += OnListViewBindingContextChanged;
        bindable.ItemAppearing += OnListViewItemAppearing;

        BindingContext = _associatedListView.BindingContext;
    }

    protected override void OnDetachingFrom(ListView bindable) {
        RemoveBinding(ItemsSourceProperty);

        bindable.BindingContextChanged -= OnListViewBindingContextChanged;
        bindable.ItemAppearing -= OnListViewItemAppearing;

        base.OnDetachingFrom(bindable);
    }

    private void OnListViewBindingContextChanged(object sender, EventArgs e) =>
        BindingContext = _associatedListView.BindingContext;

    private async void OnListViewItemAppearing(object sender,
        ItemVisibilityEventArgs e) {
        if (IsLoadingMore) {
            return;
        }

        if (_associatedListView.ItemsSource is not IMauiInfiniteScrollLoader
            loader) {
            return;
        }

        if (!loader.CanLoadMore || !ShouldLoadMore(e.Item)) {
            return;
        }

        UpdateIsLoadingMore(true, null);
        await loader.LoadMoreAsync();
        UpdateIsLoadingMore(false, null);
    }

    private bool ShouldLoadMore(object item) {
        if (_associatedListView.ItemsSource is not IList list) {
            return false;
        }

        if (list.Count == 0) {
            return true;
        }

        var lastItem = list[^1];
        return _associatedListView.IsGroupingEnabled && lastItem is IList group
            ? group.Count == 0 || group[^1] == item
            : lastItem == item;
    }

    private static void OnItemsSourceChanged(BindableObject bindable,
        object oldValue, object newValue) {
        if (bindable is not MauiInfiniteScrollBehavior behavior) {
            return;
        }

        if (oldValue is IMauiInfiniteScrollLoading oldLoading) {
            oldLoading.LoadingMore -= behavior.OnLoadingMore;
            behavior.UpdateIsLoadingMore(null, false);
        }

        if (newValue is not IMauiInfiniteScrollLoading newLoading) {
            return;
        }

        newLoading.LoadingMore += behavior.OnLoadingMore;
        behavior.UpdateIsLoadingMore(null, newLoading.IsLoadingMore);
    }

    private void OnLoadingMore(object sender, MauiLoadingMoreEventArgs e) =>
        UpdateIsLoadingMore(null, e.IsLoadingMore);

    private void UpdateIsLoadingMore(bool? fromScroll, bool? fromLoader) {
        _isLoadingMoreFromScroll = fromScroll ?? _isLoadingMoreFromScroll;
        _isLoadingMoreFromLoader = fromLoader ?? _isLoadingMoreFromLoader;

        IsLoadingMore = _isLoadingMoreFromScroll || _isLoadingMoreFromLoader;
    }
}